package net.ulrice.mdls.sensor;

import java.io.BufferedReader;
import java.io.File;
import java.io.FileReader;
import java.io.IOException;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

import net.ulrice.mdls.sensor.facade.CpuInfo;
import net.ulrice.mdls.sensor.facade.CpuLoad;
import net.ulrice.mdls.sensor.facade.MemoryInfo;
import net.ulrice.mdls.sensor.facade.NetworkInfo;

/**
 * Unix implementation gathering the sensor information from the /proc file system.
 * 
 * @author christof
 */
public class UnixProcSensor implements Sensor {

	private File cpuLoadFile;
	private File cpuInfoFile;
	private File memInfoFile;
	private File netInfoFile;

	private int numCores;
	private List<CpuInfo> cpuInfo;
	private Map<String, Long> cpuLastValueMap = new HashMap<String, Long>();
	private Map<String, Long> netLastValueMap = new HashMap<String, Long>();
	private Long netInfoLastTimestamp;


	public UnixProcSensor() throws SensorException {

		cpuInfoFile = new File("/proc/cpuinfo");
		cpuLoadFile = new File("/proc/stat");
		memInfoFile = new File("/proc/meminfo");
		netInfoFile = new File("/proc/net/dev");

		if (cpuInfoFile.exists() && cpuInfoFile.canRead() && cpuLoadFile.exists() && cpuLoadFile.canRead() && memInfoFile.exists() && memInfoFile.canRead() && netInfoFile.exists() && netInfoFile.canRead()) {

			try {
				cpuInfo = parseCpuInfo();
				numCores = cpuInfo.size();
				
				// Fill last value maps
				getCpuLoad();
				getNetworkInfo();
			} catch (IOException e) {
				throw new SensorException(e);
			}
		} else {
			throw new SensorException("Cannot read /proc filesystem.");
		}
	}

	public int getNumCores() {
		return numCores;
	}

	public List<CpuInfo> getCpuInfo() {
		return cpuInfo;
	}

	public List<CpuLoad> getCpuLoad() throws SensorException {
		try {
			return parseCpuLoad();
		} catch (IOException e) {
			throw new SensorException(e);
		}
	}

	public List<CpuLoad> parseCpuLoad() throws IOException {
		BufferedReader br = null;
		try {

			br = new BufferedReader(new FileReader(cpuLoadFile));

			String line;
			List<CpuLoad> result = new ArrayList<CpuLoad>();
			while ((line = br.readLine()) != null) {
				String[] split = line.split(" +");
				if (line.startsWith("cpu ")) {
					result.add(parseCpuLoadLine("Total", -1, split));
				} else if (line.startsWith("cpu")) {
					int coreNumber = Integer.parseInt(split[0].substring(3));
					result.add(parseCpuLoadLine(this.cpuInfo.get(coreNumber).getModelName(), coreNumber, split));
				}
			}
			return result;

		} finally {
			if (br != null) {
				br.close();
			}
		}
	}

	private CpuLoad parseCpuLoadLine(String modelName, int coreNumber, String[] split) {

		String id = "CPU" + coreNumber;
		
		long user = Long.parseLong(split[1]);
		long nice = Long.parseLong(split[2]);
		long system = Long.parseLong(split[3]);
		long idle = Long.parseLong(split[4]);
		long ioWait = Long.parseLong(split[5]);
		long irq = Long.parseLong(split[6]);
		long softIrq = Long.parseLong(split[7]);
		
		long total = user + nice + system + idle + ioWait + irq + softIrq;
		Long totalLastValue = cpuLastValueMap.get(id);
		if(totalLastValue == null) {
			totalLastValue = total;
		}
		cpuLastValueMap.put(id, total);
		total = total - totalLastValue;
		
		CpuLoad load = new CpuLoad();
		load.setCpuId(Integer.toString(coreNumber));
		load.setCpuName(modelName);
		load.setUser(calcPercentageFromDifference(id + "user", user, cpuLastValueMap, total));
		load.setNice(calcPercentageFromDifference(id + "nice", nice, cpuLastValueMap, total));
		load.setSystem(calcPercentageFromDifference(id + "system", system, cpuLastValueMap, total));
		load.setIdle(calcPercentageFromDifference(id + "idle", idle, cpuLastValueMap, total));
		load.setIowait(calcPercentageFromDifference(id + "ioWait", ioWait, cpuLastValueMap, total));
		load.setIrq(calcPercentageFromDifference(id + "irq", irq, cpuLastValueMap, total));
		load.setSoftIrq(calcPercentageFromDifference(id + "softIrq", softIrq, cpuLastValueMap, total));
		load.setTotal(load.getUser() + load.getNice() + load.getSystem());
		return load;
	}

	private double calcPercentageFromDifference(String id, Long currentValue, Map<String, Long> lastValueMap, long total) {
		Long lastValue = lastValueMap.get(id);
		lastValueMap.put(id, currentValue);
		if(lastValue == null || total == 0) {
			return 0.0;
		}
		Long value = currentValue - lastValue;
		return 100.0 / total * value;
	}

	public List<CpuInfo> parseCpuInfo() throws IOException {
		BufferedReader br = null;
		try {

			br = new BufferedReader(new FileReader(cpuInfoFile));

			String line;
			List<CpuInfo> result = new ArrayList<CpuInfo>();
			CpuInfo cpuInfo = new CpuInfo();
			while ((line = br.readLine()) != null) {

				if ("".equals(line.trim())) {
					result.add(cpuInfo);
					cpuInfo = new CpuInfo();
				} else if (line.indexOf(':') >= 0) {
					String[] attributeValue = line.split(":");
					String attribute = attributeValue[0].trim();
					if (attributeValue.length > 1) {

						String value = attributeValue[1];

						if(attribute.equals("processor")) {
							cpuInfo.setProcessor(value);
						} else if(attribute.equals("vendor_id")) {
							cpuInfo.setVendorId(value);
						} else if(attribute.equals("cpu family")) {
							cpuInfo.setCpuFamily(value);
						} else if(attribute.equals("model")) {
							cpuInfo.setModel(value);
						} else if(attribute.equals("model name")) {
							cpuInfo.setModelName(value);
						} else if(attribute.equals("stepping")) {
							cpuInfo.setStepping(value);
						} else if(attribute.equals("microcode")) {
							cpuInfo.setMicrocode(value);
						} else if(attribute.equals("cpu MHz")) {
							cpuInfo.setCpuMhz(value);
						} else if(attribute.equals("cache size")) {
							cpuInfo.setCacheSize(value);
						} else if(attribute.equals("physical id")) {
							cpuInfo.setPhysicalId(value);
						} else if(attribute.equals("siblings")) {
							cpuInfo.setSiblings(value);
						} else if(attribute.equals("core id")) {
							cpuInfo.setCoreId(value);
						} else if(attribute.equals("cpu cores")) {
							cpuInfo.setCpuCores(value);
						} else if(attribute.equals("apicid")) {
							cpuInfo.setApicId(value);
						} else if(attribute.equals("initial apicid")) {
							cpuInfo.setInitialApicId(value);
						} else if(attribute.equals("fpu")) {
							cpuInfo.setFpu(value);
						} else if(attribute.equals("fpu_exception")) {
							cpuInfo.setFpuException(value);
						} else if(attribute.equals("cpuid level")) {
							cpuInfo.setCpuIdLevel(value);
						} else if(attribute.equals("wp")) {
							cpuInfo.setWp(value);
						} else if(attribute.equals("flags")) {
							cpuInfo.setFlags(value);
						} else if(attribute.equals("bogomips")) {
							cpuInfo.setBogomips(value);
						} else if(attribute.equals("clflush size")) {
							cpuInfo.setClFlushSize(value);
						} else if(attribute.equals("cache_alignment")) {
							cpuInfo.setCacheAlignment(value);
						} else if(attribute.equals("address sizes")) {
							cpuInfo.setAddressSized(value);
						} else if(attribute.equals("power management")) {
							cpuInfo.setPowerManagement(value);
						}
					}

				}
			}
			return result;

		} finally {
			if (br != null) {
				br.close();
			}
		}
	}
	
	@Override
	public MemoryInfo getMemoryInfo() throws SensorException {
		BufferedReader br = null;
		try {
			br = new BufferedReader(new FileReader(memInfoFile));

			MemoryInfo result = new MemoryInfo();
			
			String line;
			while ((line = br.readLine()) != null) {
				if (line.indexOf(':') >= 0) {
					String[] attributeValue = line.split(":");
					if (attributeValue[0].startsWith("MemTotal")) {
						result.setTotalMemory(Long.parseLong(parseMemoryValue(attributeValue)));
					} else if (attributeValue[0].startsWith("MemFree")) {
						result.setFreeMemory(Long.parseLong(parseMemoryValue(attributeValue)));
					} else if (attributeValue[0].startsWith("SwapTotal")) {
						result.setTotalSwap(Long.parseLong(parseMemoryValue(attributeValue)));
					} else if (attributeValue[0].startsWith("SwapFree")) {
						result.setFreeSwap(Long.parseLong(parseMemoryValue(attributeValue)));
					}
				}
			}
			
			result.setUsedMemory(result.getTotalMemory() - result.getFreeMemory());
			result.setUsedMemoryPercentage(100.0/result.getTotalMemory() * result.getUsedMemory());
			
			result.setUsedSwap(result.getTotalSwap() - result.getFreeSwap());
			result.setUsedSwapPercentage(100.0/result.getTotalSwap() * result.getUsedSwap());
			
			return result;
		} catch(IOException e) {
			throw new SensorException(e);
		} finally {
			if (br != null) {
				try {
					br.close();
				} catch (IOException e) {
					throw new SensorException(e);
				}
			}
		}
	}

	private String parseMemoryValue(String[] attributeValue) {
		return attributeValue[1].trim().split(" ")[0];
	}
	

	@Override
	public List<NetworkInfo> getNetworkInfo() throws SensorException {
		BufferedReader br = null;
		try {
			br = new BufferedReader(new FileReader(netInfoFile));

			List<NetworkInfo> result = new ArrayList<NetworkInfo>();
			
			String line;
			while ((line = br.readLine()) != null) {
				if (line.indexOf(':') >= 0) {
					String[] split = line.split(" +");
					result.add(parseNetworkInfoLine(split));
				}
			}
			return result;
		} catch(IOException e) {
			throw new SensorException(e);
		} finally {
			if (br != null) {
				try {
					br.close();
				} catch (IOException e) {
					throw new SensorException(e);
				}
			}
		}
	}

	private NetworkInfo parseNetworkInfoLine(String[] values) {
		NetworkInfo result = new NetworkInfo();
		result.setInterfaceName(values[1].substring(0, values[1].length() - 1).trim());


		long timeDiff = -1;
		if(netInfoLastTimestamp != null) {
			timeDiff = (System.currentTimeMillis() - netInfoLastTimestamp);
		}

		long receivedBytes = Long.parseLong(values[2]);
		result.setBytesReceived(receivedBytes);
		Long lastRcvValue = netLastValueMap.get(result.getInterfaceName() + "RCV");
		netLastValueMap.put(result.getInterfaceName() + "RCV", receivedBytes);
		if(lastRcvValue == null || timeDiff < 100) {
			result.setBytesReceivedPerSecond(0);
		} else {
			result.setBytesReceivedPerSecond((receivedBytes - lastRcvValue) / (timeDiff/1000.0));
		}
				
		long sendBytes = Long.parseLong(values[10]);
		result.setBytesSend(sendBytes);
		Long lastSndValue = netLastValueMap.get(result.getInterfaceName() + "SND");
		netLastValueMap.put(result.getInterfaceName() + "SND", sendBytes);
		if(lastSndValue == null || timeDiff < 100) {
			result.setBytesSendPerSecond(0);
		} else {
			result.setBytesSendPerSecond((sendBytes - lastSndValue) / (timeDiff/1000.0));
		}
		
		netInfoLastTimestamp = System.currentTimeMillis();
		
		return result;
	}
}
